// This file is part of Keepass2Android, Copyright 2025 Philipp Crocoll.
//
//   Keepass2Android is free software: you can redistribute it and/or modify
//   it under the terms of the GNU General Public License as published by
//   the Free Software Foundation, either version 3 of the License, or
//   (at your option) any later version.
//
//   Keepass2Android is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU General Public License for more details.
//
//   You should have received a copy of the GNU General Public License
//   along with Keepass2Android.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.App.Slices;
using Android.Content;
using Android.Content.PM;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Preferences;
using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Java.Util.Concurrent.Atomic;
using keepass2android.services.AutofillBase.model;
using keepass2android;
using KeePassLib;
using Kp2aAutofillParser;

namespace keepass2android.services.AutofillBase
{
  public interface IAutofillIntentBuilder
  {
    PendingIntent GetAuthPendingIntentForResponse(Context context, string query, string queryDomain, string queryPackage, bool autoReturnFromQuery, AutofillServiceBase.DisplayWarning warning);

    PendingIntent GetAuthPendingIntentForWarning(Context context, PwUuid entryUuid, AutofillServiceBase.DisplayWarning warning);

    PendingIntent GetDisablePendingIntentForResponse(Context context, string query, bool isDisable);
    Intent GetRestartAppIntent(Context context);

    int AppIconResource { get; }
  }

  public abstract class AutofillServiceBase : AutofillService
  {
    private HashSet<string> _internal_blacklistedUris = null;

    public HashSet<string> BlacklistedUris
    {
      get
      {
        if (_internal_blacklistedUris == null)
        {
          _internal_blacklistedUris = new HashSet<string>()
                    {
                        KeePass.AndroidAppScheme + "android",
                        KeePass.AndroidAppScheme + "com.android.settings",
                        KeePass.AndroidAppScheme + this.PackageName
                    };
        }

        return _internal_blacklistedUris;

      }
    }
    protected override void AttachBaseContext(Context baseContext)
    {
      base.AttachBaseContext(LocaleManager.setLocale(baseContext));
    }

    //use a lock to avoid returning a response several times in buggy Firefox during one connection: this avoids flickering 
    //and disappearing of the autofill prompt.
    //Instead of using a boolean lock, we use a "time-out lock" which is cleared after a few seconds
    private DateTime _lockTime = DateTime.MinValue;

    private TimeSpan _lockTimeout = TimeSpan.FromSeconds(2);

    public AutofillServiceBase()
    {

    }

    public AutofillServiceBase(IntPtr javaReference, JniHandleOwnership transfer)
        : base(javaReference, transfer)
    {
    }

    public static HashSet<string> CompatBrowsers = new HashSet<string>
        {
            "org.mozilla.firefox",
            "org.mozilla.firefox_beta",
            "com.microsoft.emmx",
            "com.android.chrome",
            "com.chrome.beta",
            "com.android.browser",
            "com.brave.browser",
            "com.opera.browser",
            "com.opera.browser.beta",
            "com.opera.mini.native",
            "com.chrome.dev",
            "com.chrome.canary",
            "com.google.android.apps.chrome",
            "com.google.android.apps.chrome_dev",
            "com.yandex.browser",
            "com.sec.android.app.sbrowser",
            "com.sec.android.app.sbrowser.beta",
            "org.codeaurora.swe.browser",
            "com.amazon.cloud9",
            "mark.via.gp",
            "org.bromite.bromite",
            "org.chromium.chrome",
            "com.kiwibrowser.browser",
            "com.ecosia.android",
            "com.opera.mini.native.beta",
            "org.mozilla.fennec_aurora",
            "org.mozilla.fennec_fdroid",
            "com.qwant.liberty",
            "com.opera.touch",
            "org.mozilla.fenix",
            "org.mozilla.fenix.nightly",
            "org.mozilla.reference.browser",
            "org.mozilla.rocket",
            "org.torproject.torbrowser",
            "com.vivaldi.browser",
        };

    public override void OnFillRequest(FillRequest request, CancellationSignal cancellationSignal, FillCallback callback)
    {
      bool isManual = (request.Flags & FillRequest.FlagManualRequest) != 0;
      CommonUtil.logd("onFillRequest " + (isManual ? "manual" : "auto"));
      var structure = request.FillContexts.Last().Structure;


      if (_lockTime + _lockTimeout < DateTime.Now)
      {
        _lockTime = DateTime.Now;

        //TODO support package signature verification as soon as this is supported in Keepass storage

        var clientState = request.ClientState;
        CommonUtil.logd("onFillRequest(): data=" + CommonUtil.BundleToString(clientState));


        cancellationSignal.CancelEvent += (sender, e) =>
        {
          Kp2aLog.Log("Cancel autofill not implemented yet.");
          _lockTime = DateTime.MinValue;
        };
        // Parse AutoFill data in Activity
        StructureParser.AutofillTargetId query = null;
        var parser = new StructureParser(this, structure);
        try
        {
          query = parser.ParseForFill();

        }
        catch (Java.Lang.SecurityException e)
        {
          Log.Warn(CommonUtil.Tag, "Security exception handling request");
          callback.OnFailure(Util.GetErrorMessage(e));
          return;
        }


        InlineSuggestionsRequest inlineSuggestionsRequest = null;
        IList<InlinePresentationSpec> inlinePresentationSpecs = null;
        if (((int)Build.VERSION.SdkInt >= 30)
            && (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(GetString(Resource.String.InlineSuggestions_key), true)))
        {
          inlineSuggestionsRequest = request.InlineSuggestionsRequest;

          inlinePresentationSpecs = inlineSuggestionsRequest?.InlinePresentationSpecs;
        }


        var autofillIds = parser.AutofillFields.GetAutofillIds();
        if (autofillIds.Length != 0 && CanAutofill(query, isManual))
        {
          var responseBuilder = new FillResponse.Builder();

          bool hasEntryDataset = false;

          IList<Dataset> entryDatasets = new List<Dataset>();
          if (query.IncompatiblePackageAndDomain == false)
          {
            Kp2aLog.Log("AF: (query.IncompatiblePackageAndDomain == false)");
            //domain and package are compatible. Use Domain if available and package otherwise. Can fill without warning.
            entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
                                        query.PackageName,
                                        autofillIds, parser, DisplayWarning.None,
                                        inlinePresentationSpecs
                                        ).Where(ds => ds != null).ToList();
            if (entryDatasets.Count > inlineSuggestionsRequest?.MaxSuggestionCount - 2 /*disable dataset and query*/)
            {
              //we have too many elements. disable inline suggestions
              inlinePresentationSpecs = null;
              entryDatasets = BuildEntryDatasets(query.DomainOrPackage, query.WebDomain,
                                      query.PackageName,
                                      autofillIds, parser, DisplayWarning.None,
                                      null
                                      ).Where(ds => ds != null).ToList();
            }
            foreach (var entryDataset in entryDatasets
            )
            {
              Kp2aLog.Log("AF: Got EntryDataset " + (entryDataset == null));
              responseBuilder.AddDataset(entryDataset);
              hasEntryDataset = true;
            }
          }



          {
            if (query.WebDomain != null)
              AddQueryDataset(query.WebDomain,
                  query.WebDomain, query.PackageName, autofillIds, responseBuilder, !hasEntryDataset,
                  query.IncompatiblePackageAndDomain
                      ? DisplayWarning.FillDomainInUntrustedApp
                      : DisplayWarning.None,
                  AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
            else
              AddQueryDataset(query.PackageNameWithPseudoSchema,
                  query.WebDomain, query.PackageName, autofillIds, responseBuilder, !hasEntryDataset, DisplayWarning.None,
                  AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));
          }

          if (!PreferenceManager.GetDefaultSharedPreferences(this)
              .GetBoolean(GetString(Resource.String.NoAutofillDisabling_key), false))
            AddDisableDataset(query.DomainOrPackage, autofillIds, responseBuilder, isManual, AutofillHelper.ExtractSpec(inlinePresentationSpecs, entryDatasets.Count));

          if (PreferenceManager.GetDefaultSharedPreferences(this)
              .GetBoolean(GetString(Resource.String.OfferSaveCredentials_key), true))
          {
            if (!CompatBrowsers.Contains(parser.PackageId))
            {
              responseBuilder.SetSaveInfo(new SaveInfo.Builder(parser.AutofillFields.SaveType,
                  parser.AutofillFields.GetAutofillIds()).Build());
            }

          }

          Kp2aLog.Log("return autofill success");
          callback.OnSuccess(responseBuilder.Build());
        }
        else
        {
          Kp2aLog.Log("cannot autofill");
          callback.OnSuccess(null);
        }
      }
      else
      {
        Kp2aLog.Log("Ignoring onFillRequest as there is another request going on.");
      }
    }




    private List<Dataset> BuildEntryDatasets(string query, string queryDomain, string queryPackage, AutofillId[] autofillIds, StructureParser parser,
        DisplayWarning warning, IList<InlinePresentationSpec> inlinePresentationSpecs)
    {
      List<Dataset> result = new List<Dataset>();
      Kp2aLog.Log("AF: BuildEntryDatasets");
      Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> suggestedEntries = GetSuggestedEntries(query);
      Kp2aLog.Log("AF: BuildEntryDatasets found " + suggestedEntries.Count + " entries");

      int count = 0;

      var totpHelper = new Kp2aTotp();

      foreach (var kvp in suggestedEntries)
      {
        var filledAutofillFieldCollection = kvp.Value;
        PwEntryOutput entry = kvp.Key;

        if (filledAutofillFieldCollection == null)
          continue;

        var inlinePresentationSpec = AutofillHelper.ExtractSpec(inlinePresentationSpecs, count);

        if ((warning == DisplayWarning.None)
            && (totpHelper.TryGetAdapter(entry) == null))
        {
          //no special dataset, we can immediately return the field collection
          FilledAutofillFieldCollection<ViewNodeInputField> partitionData =
              AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints);

          Kp2aLog.Log("AF: Add dataset");

          result.Add(AutofillHelper.NewDataset(this, parser.AutofillFields, partitionData, IntentBuilder,
              inlinePresentationSpec));
        }
        else
        {

          //return an "auth" dataset (actually for just warning the user in case domain/package dont match and/or to make sure that we open the EntryActivity,
          // thus opening the entry notification in case of TOTP)
          PendingIntent pendingIntent =
              IntentBuilder.GetAuthPendingIntentForWarning(this, entry.Uuid, warning);
          var datasetName = filledAutofillFieldCollection.DatasetName;
          if (datasetName == null)
          {
            Kp2aLog.Log("AF: dataset name is null");
            continue;
          }

          RemoteViews presentation =
              AutofillHelper.NewRemoteViews(PackageName, datasetName, AppNames.LauncherIcon);

          var datasetBuilder = new Dataset.Builder(presentation);
          datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);

          AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, datasetName, datasetBuilder, AppNames.LauncherIcon, null);

          //need to add placeholders so we can directly fill after ChooseActivity
          foreach (var autofillId in autofillIds)
          {
            datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
          }
          Kp2aLog.Log("AF: Add auth dataset");
          result.Add(datasetBuilder.Build());
        }
        count++;
      }

      return result;


    }

    protected abstract Dictionary<PwEntryOutput, FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query);

    public enum DisplayWarning
    {
      None,
      FillDomainInUntrustedApp, //display a warning that the user is filling credentials for a domain inside an app not marked as trusted browser

    }

    private void AddQueryDataset(string query, string queryDomain, string queryPackage, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool autoReturnFromQuery, DisplayWarning warning, InlinePresentationSpec inlinePresentationSpec)
    {
      PendingIntent pendingIntent = IntentBuilder.GetAuthPendingIntentForResponse(this, query, queryDomain, queryPackage, autoReturnFromQuery, warning);
      string text = GetString(Resource.String.autofill_sign_in_prompt);
      RemoteViews overlayPresentation = AutofillHelper.NewRemoteViews(base.PackageName,
          text, AppNames.LauncherIcon);

      var datasetBuilder = new Dataset.Builder(overlayPresentation);
      datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);
      //need to add placeholders so we can directly fill after ChooseActivity
      foreach (var autofillId in autofillIds)
      {
        datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
      }

      AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, AppNames.LauncherIcon, pendingIntent);


      responseBuilder.AddDataset(datasetBuilder.Build());
    }
    public static string GetDisplayNameForQuery(string str, Context Context)
    {
      string displayName = str;
      try
      {
        string appPrefix = KeePass.AndroidAppScheme;
        if (str.StartsWith(appPrefix))
        {
          str = str.Substring(appPrefix.Length);
          PackageManager pm = Context.PackageManager;
          ApplicationInfo ai;
          try
          {
            ai = pm.GetApplicationInfo(str, 0);
          }
          catch (PackageManager.NameNotFoundException e)
          {
            ai = null;
          }
          displayName = ai != null ? pm.GetApplicationLabel(ai) : str;
        }
      }
      catch (Exception e)
      {
        Kp2aLog.LogUnexpectedError(e);
      }

      return displayName;
    }

    private void AddDisableDataset(string query, AutofillId[] autofillIds, FillResponse.Builder responseBuilder, bool isManual, InlinePresentationSpec inlinePresentationSpec)
    {
      bool isQueryDisabled = IsQueryDisabled(query);
      if (isQueryDisabled && !isManual)
        return;
      bool isForDisable = !isQueryDisabled;
      var pendingIntent = IntentBuilder.GetDisablePendingIntentForResponse(this, query, isForDisable);

      string text = GetString(isForDisable ? Resource.String.autofill_disable : Resource.String.autofill_enable_for, new Java.Lang.Object[] { GetDisplayNameForQuery(query, this) });
      RemoteViews presentation = AutofillHelper.NewRemoteViews(base.PackageName,
          text, Resource.Drawable.baseline_close_24);

      var datasetBuilder = new Dataset.Builder(presentation);
      datasetBuilder.SetAuthentication(pendingIntent?.IntentSender);

      AutofillHelper.AddInlinePresentation(this, inlinePresentationSpec, text, datasetBuilder, Resource.Drawable.baseline_close_24, pendingIntent);

      foreach (var autofillId in autofillIds)
      {
        datasetBuilder.SetValue(autofillId, AutofillValue.ForText("PLACEHOLDER"));
      }

      responseBuilder.AddDataset(datasetBuilder.Build());
    }

    private bool CanAutofill(StructureParser.AutofillTargetId query, bool isManual)
    {
      if (BlacklistedUris.Contains(query.PackageNameWithPseudoSchema))
        return false;
      if (!isManual)
      {
        var isQueryDisabled = IsQueryDisabled(query.DomainOrPackage);
        if (isQueryDisabled)
          return false;
      }
      return true;
    }

    private bool IsQueryDisabled(string query)
    {
      var prefs = PreferenceManager.GetDefaultSharedPreferences(this);
      var disabledValues = prefs.GetStringSet("AutoFillDisabledQueries", new List<string>());

      bool isQueryDisabled = disabledValues.Contains(query);
      return isQueryDisabled;
    }

    public override void OnSaveRequest(SaveRequest request, SaveCallback callback)
    {

      var structure = request.FillContexts?.LastOrDefault()?.Structure;
      if (structure == null)
      {
        return;
      }

      var parser = new StructureParser(this, structure);
      var query = parser.ParseForSave();
      try
      {
        HandleSaveRequest(parser, query);
        callback.OnSuccess();
      }
      catch (Exception e)
      {
        callback.OnFailure(Util.GetErrorMessage(e));
      }

    }

    protected abstract void HandleSaveRequest(StructureParser parser, StructureParser.AutofillTargetId query);


    public override void OnConnected()
    {
      CommonUtil.logd("onConnected");
    }

    public override void OnDisconnected()
    {

      _lockTime = DateTime.MinValue;
      CommonUtil.logd("onDisconnected");
    }

    public abstract IAutofillIntentBuilder IntentBuilder { get; }
  }
}
